/*
 * Copyright (C) 2015 Square, Inc.
 * Adapted for ArkTS/HarmonyOS
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
  canParseAsIpAddress,
  indexOfFirstNonAsciiWhitespace,
  indexOfLastNonAsciiWhitespace,
  toCanonicalHost
} from './utils/StringUtil';
import { uri, url, util } from '@kit.ArkTS';
import { IllegalArgumentException, IllegalStateException, IndexOutOfBoundsException,
  RuntimeException } from './Error/error';
import { kotlinRequire } from './utils/Utils';

/**
 * A uniform resource locator (URL) with a scheme of either `http` or `https`.
 * 
 * This class provides comprehensive URL parsing, composition, and manipulation
 * for HTTP/HTTPS URLs with full support for encoding, query parameters,
 * path segments, and relative URL resolution.
 */
export class HttpUrl {
  /** Either "http" or "https". */
  readonly scheme: string;
  
  /** The decoded username, or an empty string if none is present. */
  readonly username: string;
  
  /** The decoded password, or an empty string if none is present. */
  readonly password: string;
  
  /** The host address (hostname, IPv4, or IPv6). */
  readonly host: string;
  
  /** The port number (80 for HTTP, 443 for HTTPS by default). */
  readonly port: number;
  
  /** Decoded path segments. */
  readonly pathSegments: string[];
  
  /** Alternating decoded query names and values, or null for no query. */
  private readonly queryNamesAndValues: (string | null)[] | null;
  
  /** The decoded fragment, or null if none. */
  readonly fragment: string | null;
  
  /** Canonical URL string. */
  private readonly url: string;

  constructor(
    scheme: string,
    username: string,
    password: string,
    host: string,
    port: number,
    pathSegments: string[],
    queryNamesAndValues: (string | null)[] | null,
    fragment: string | null,
    url: string
  ) {
    this.scheme = scheme;
    this.username = username;
    this.password = password;
    this.host = host;
    this.port = port;
    this.pathSegments = pathSegments;
    this.queryNamesAndValues = queryNamesAndValues;
    this.fragment = fragment;
    this.url = url;
  }


  get isHttps(): boolean {
    return this.scheme === 'https';
  }

  /**
   * Returns this URL as an ArkTS URL object.
   * @returns URL object representing this HttpUrl
   */
  toUrl(): url.URL {
    try {
      return new url.URL(this.url);
    } catch (e) {
      throw new RuntimeException(`Failed to create URL: ${(e as Error).message}`);
    }
  }

  /**
   * Returns this URL as an ArkTS URI object. Because URI is more strict than this class,
   * the returned URI may be semantically different from this URL:
   *
   * - Characters forbidden by URI like `[` and `|` will be escaped.
   * - Invalid percent-encoded sequences like `%xx` will be encoded like `%25xx`.
   * - Whitespace and control characters in the fragment will be stripped.
   *
   * These differences may have a significant consequence when the URI is interpreted by a
   * web server. For this reason the URI class and this method should be avoided.
   * 
   * @returns URI object representing this HttpUrl
   */
  toUri(): uri.URI {
    try {
      // First, try to create URI with re-encoded URL
      const reencoded = this.newBuilder().reencodeForUri().toString();
      return new uri.URI(reencoded);
    } catch (e) {
      // Unlikely edge case: the URI has a forbidden character in the fragment. Strip it & retry.
      try {
        const reencoded = this.newBuilder().reencodeForUri().toString();
        // Strip control characters and whitespace
        const stripped = reencoded.replace(/[\u0000-\u001F\u007F-\u009F\s]/g, '');
        return new uri.URI(stripped);
      } catch (e1) {
        throw new RuntimeException(`Failed to create URI: ${(e as Error).message}`);
      }
    }
  }

  /**
   * The encoded username, or an empty string if none is set.
   */
  get encodedUsername(): string {
    if (this.username.length === 0) return '';
    const usernameStart = this.scheme.length + 3; // "://".length
    const usernameEnd = this.delimiterOffsetInternal(this.url, ':@', usernameStart, this.url.length);
    return this.url.substring(usernameStart, usernameEnd);
  }

  /**
   * The encoded password, or an empty string if none is set.
   */
  get encodedPassword(): string {
    if (this.password.length === 0) return '';
    const passwordStart = this.url.indexOf(':', this.scheme.length + 3) + 1;
    const passwordEnd = this.url.indexOf('@');
    return this.url.substring(passwordStart, passwordEnd);
  }

  /**
   * The number of segments in this URL's path.
   */
  get pathSize(): number {
    return this.pathSegments.length;
  }

  /**
   * The entire path of this URL encoded for use in HTTP resource resolution.
   */
  get encodedPath(): string {
    const pathStart = this.url.indexOf('/', this.scheme.length + 3);
    const pathEnd = this.delimiterOffsetInternal(this.url, '?#', pathStart, this.url.length);
    return this.url.substring(pathStart, pathEnd);
  }

  /**
   * A list of encoded path segments.
   */
  get encodedPathSegments(): string[] {
    const pathStart = this.url.indexOf('/', this.scheme.length + 3);
    const pathEnd = this.delimiterOffsetInternal(this.url, '?#', pathStart, this.url.length);
    const result: string[] = [];
    let i = pathStart;
    while (i < pathEnd) {
      i++; // Skip the '/'
      const segmentEnd = this.delimiterOffsetChar(this.url, '/', i, pathEnd);
      result.push(this.url.substring(i, segmentEnd));
      i = segmentEnd;
    }
    return result;
  }

  /**
   * The query of this URL, encoded for use in HTTP resource resolution.
   */
  get encodedQuery(): string | null {
    if (this.queryNamesAndValues === null) return null;
    const queryStart = this.url.indexOf('?') + 1;
    const queryEnd = this.delimiterOffsetChar(this.url, '#', queryStart, this.url.length);
    return this.url.substring(queryStart, queryEnd);
  }

  /**
   * This URL's decoded query.
   */
  get query(): string | null {
    if (this.queryNamesAndValues === null) return null;
    return HttpUrl.toQueryString(this.queryNamesAndValues);
  }

  /**
   * The number of query parameters in this URL.
   */
  get querySize(): number {
    return this.queryNamesAndValues !== null ? this.queryNamesAndValues.length / 2 : 0;
  }

  /**
   * Returns the first query parameter named `name`, or null if there is no such parameter.
   */
  queryParameter(name: string): string | null {
    if (this.queryNamesAndValues === null) return null;
    for (let i = 0; i < this.queryNamesAndValues.length; i += 2) {
      if (name === this.queryNamesAndValues[i]) {
        return this.queryNamesAndValues[i + 1];
      }
    }
    return null;
  }

  /**
   * Returns the distinct query parameter names in this URL.
   */
  get queryParameterNames(): Set<string> {
    if (this.queryNamesAndValues === null) return new Set();
    const result = new Set<string>();
    for (let i = 0; i < this.queryNamesAndValues.length; i += 2) {
      result.add(this.queryNamesAndValues[i]!);
    }
    return result;
  }

  /**
   * Returns all values for the query parameter `name`.
   */
  queryParameterValues(name: string): (string | null)[] {
    if (this.queryNamesAndValues === null) return [];
    const result: (string | null)[] = [];
    for (let i = 0; i < this.queryNamesAndValues.length; i += 2) {
      if (name === this.queryNamesAndValues[i]) {
        result.push(this.queryNamesAndValues[i + 1]);
      }
    }
    return result;
  }

  /**
   * Returns the name of the query parameter at `index`.
   */
  queryParameterName(index: number): string {
    if (this.queryNamesAndValues === null) {
      throw new IndexOutOfBoundsException('No query parameters');
    }
    if (index < 0 || index >= this.querySize) {
      throw new IndexOutOfBoundsException(`Index out of bounds: ${index}, size: ${this.querySize}`);
    }
    return this.queryNamesAndValues[index * 2]!;
  }

  /**
   * Returns the value of the query parameter at `index`.
   */
  queryParameterValue(index: number): string | null {
    if (this.queryNamesAndValues === null) {
      throw new IndexOutOfBoundsException('No query parameters');
    }
    if (index < 0 || index >= this.querySize) {
      throw new IndexOutOfBoundsException(`Index out of bounds: ${index}, size: ${this.querySize}`);
    }
    return this.queryNamesAndValues[index * 2 + 1];
  }

  /**
   * This URL's encoded fragment.
   */
  get encodedFragment(): string | null {
    if (this.fragment === null) return null;
    const fragmentStart = this.url.indexOf('#') + 1;
    return this.url.substring(fragmentStart);
  }

  /**
   * Returns a string with username, password, query, and fragment redacted.
   */
  redact(): string {
    return this.newBuilderWithLink('/...')!
      .username('')
      .password('')
      .build()
      .toString();
  }

  /**
   * Returns the URL that would be retrieved by following `link` from this URL.
   */
  resolve(link: string): HttpUrl | null {
    const builder = this.newBuilderWithLink(link);
    return builder ? builder.build() : null;
  }

  /**
   * Returns a builder based on this URL.
   */
  newBuilder(): HttpUrlBuilder {
    const result = new HttpUrlBuilder();
    result.$scheme = this.scheme;
    result.$encodedUsername = this.encodedUsername;
    result.$encodedPassword = this.encodedPassword;
    result.$host = this.host;
    result.$port = this.port !== HttpUrl.defaultPort(this.scheme) ? this.port : -1;
    result.$encodedPathSegments = [...this.encodedPathSegments];
    result.encodedQuery(this.encodedQuery);
    result.$encodedFragment = this.encodedFragment;
    return result;
  }

  /**
   * Returns a builder for the URL that would be retrieved by following `link` from this URL.
   */
  newBuilderWithLink(link: string): HttpUrlBuilder | null {
    try {
      return new HttpUrlBuilder().parse(this, link);
    } catch (e) {
      return null;
    }
  }

  equals(other: HttpUrl | null): boolean {
    if (other === null) return false;
    return other.url === this.url;
  }

  hashCode(): number {
    let hash = 0;
    for (let i = 0; i < this.url.length; i++) {
      hash = ((hash << 5) - hash) + this.url.charCodeAt(i);
      hash = hash & hash; // Convert to 32bit integer
    }
    return hash;
  }

  toString(): string {
    return this.url;
  }

  /**
   * Returns the domain name that is one level beneath the public suffix.
   */
  topPrivateDomain(): string | null {
    if (canParseAsIpAddress(this.host)) {
      return null;
    }
    // Simplified implementation - in production, use a public suffix list
    const parts = this.host.split('.');
    if (parts.length >= 2) {
      return parts.slice(-2).join('.');
    }
    return null;
  }

  // Helper methods
  private delimiterOffsetInternal(input: string, delimiters: string, pos: number, limit: number): number {
    for (let i = pos; i < limit; i++) {
      if (delimiters.indexOf(input.charAt(i)) !== -1) {
        return i;
      }
    }
    return limit;
  }

  private delimiterOffsetChar(input: string, delimiter: string, pos: number, limit: number): number {
    for (let i = pos; i < limit; i++) {
      if (input.charAt(i) === delimiter) {
        return i;
      }
    }
    return limit;
  }

  // Static methods and constants
  /**
   * Builder class alias for compatibility.
   * Allows usage like: HttpUrl.Builder.defaultPort()
   */
  static Builder: typeof HttpUrlBuilder;

  private static readonly HEX_DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
  private static readonly USERNAME_ENCODE_SET = ' "\':<=>@[]^`{}|/\\?#';
  private static readonly PASSWORD_ENCODE_SET = ' "\':<=>@[]^`{}|/\\?#';
  private static readonly PATH_SEGMENT_ENCODE_SET = ' "<>^`{}|/\\?#';
  private static readonly PATH_SEGMENT_ENCODE_SET_URI = '[]';
  private static readonly QUERY_ENCODE_SET = ' "\'<>#';
  private static readonly QUERY_COMPONENT_REENCODE_SET = ' "\'<>#&=';
  private static readonly QUERY_COMPONENT_ENCODE_SET = ' !"#$&\'(),/:;<=>?@[]\\^`{|}~';
  private static readonly QUERY_COMPONENT_ENCODE_SET_URI = '\\^`{|}';
  private static readonly FORM_ENCODE_SET = ' "\':<=>@[]^`{}|/\\?#&!$(),~';
  private static readonly FRAGMENT_ENCODE_SET = '';
  private static readonly FRAGMENT_ENCODE_SET_URI = ' "#<>\\^`{|}';

  /**
   * Returns default port for scheme (80 for http, 443 for https).
   */
  static defaultPort(scheme: string): number {
    if (scheme === 'http') return 80;
    if (scheme === 'https') return 443;
    return -1;
  }

  /**
   * Converts a string to an HttpUrl.
   */
  static StringToHttpUrl(url: string): HttpUrl {
    return new HttpUrlBuilder().parse(null, url).build();
  }

  /**
   * Converts a string to an HttpUrl, returning null if invalid.
   */
  static StringToHttpUrlOrNull(url: string): HttpUrl | null {
    try {
      return HttpUrl.StringToHttpUrl(url);
    } catch (e) {
      return null;
    }
  }

  /**
   * Converts an ArkTS URI object to an HttpUrl.
   */
  static fromUri(uriObj: uri.URI): HttpUrl {
    return HttpUrl.StringToHttpUrl(uriObj.toString());
  }

  /**
   * Converts an ArkTS URI object to an HttpUrl, returning null if invalid.
   */
  static fromUriOrNull(uriObj: uri.URI): HttpUrl | null {
    try {
      return HttpUrl.fromUri(uriObj);
    } catch (e) {
      return null;
    }
  }

  /**
   * Converts an ArkTS URL object to an HttpUrl.
   */
  static fromUrl(urlObj: url.URL): HttpUrl {
    return HttpUrl.StringToHttpUrl(urlObj.href);
  }

  /**
   * Converts an ArkTS URL object to an HttpUrl, returning null if invalid.
   */
  static fromUrlOrNull(urlObj: url.URL): HttpUrl | null {
    try {
      return HttpUrl.fromUrl(urlObj);
    } catch (e) {
      return null;
    }
  }

  /**
   * Returns a path string for path segments.
   */
  static toPathString(pathSegments: string[]): string {
    let result = '';
    for (let i = 0; i < pathSegments.length; i++) {
      result += '/';
      result += pathSegments[i];
    }
    return result;
  }

  /**
   * Returns a query string for query names and values.
   */
  static toQueryString(queryNamesAndValues: (string | null)[]): string {
    let result = '';
    for (let i = 0; i < queryNamesAndValues.length; i += 2) {
      const name = queryNamesAndValues[i];
      const value = queryNamesAndValues[i + 1];
      if (i > 0) result += '&';
      result += name;
      if (value !== null) {
        result += '=';
        result += value;
      }
    }
    return result;
  }

  /**
   * Parses a query string into name-value pairs.
   */
  static toQueryNamesAndValues(query: string): (string | null)[] {
    const result: (string | null)[] = [];
    let pos = 0;
    while (pos <= query.length) {
      let ampersandOffset = query.indexOf('&', pos);
      if (ampersandOffset === -1) ampersandOffset = query.length;

      const equalsOffset = query.indexOf('=', pos);
      if (equalsOffset === -1 || equalsOffset > ampersandOffset) {
        result.push(query.substring(pos, ampersandOffset));
        result.push(null);
      } else {
        result.push(query.substring(pos, equalsOffset));
        result.push(query.substring(equalsOffset + 1, ampersandOffset));
      }
      pos = ampersandOffset + 1;
    }
    return result;
  }

  /**
   * Decodes percent-encoded string.
   */
  static percentDecode(encoded: string, pos: number = 0, limit: number = encoded.length, plusIsSpace: boolean = false): string {
    for (let i = pos; i < limit; i++) {
      const c = encoded.charAt(i);
      if (c === '%' || (c === '+' && plusIsSpace)) {
        // Slow path: decode required
        const bytes: number[] = [];
        for (let j = pos; j < i; j++) {
          bytes.push(encoded.charCodeAt(j));
        }
        
        for (let j = i; j < limit; j++) {
          const ch = encoded.charAt(j);
          if (ch === '%' && j + 2 < limit) {
            const d1 = HttpUrl.parseHexDigit(encoded.charAt(j + 1));
            const d2 = HttpUrl.parseHexDigit(encoded.charAt(j + 2));
            if (d1 !== -1 && d2 !== -1) {
              bytes.push((d1 << 4) + d2);
              j += 2;
              continue;
            }
          } else if (ch === '+' && plusIsSpace) {
            bytes.push(32); // space
            continue;
          }
          bytes.push(ch.charCodeAt(0));
        }
        
        const decoder = new util.TextDecoder('utf-8');
        return decoder.decodeToString(new Uint8Array(bytes));
      }
    }
    // Fast path: no decoding needed
    return encoded.substring(pos, limit);
  }

  /**
   * Returns a substring of `input` on the range `[pos..limit)` with the following
   * transformations:
   *
   *  * Tabs, newlines, form feeds and carriage returns are skipped.
   *
   *  * In queries, ' ' is encoded to '+' and '+' is encoded to "%2B".
   *
   *  * Characters in `encodeSet` are percent-encoded.
   *
   *  * Control characters and non-ASCII characters are percent-encoded.
   *
   *  * All other characters are copied without transformation.
   *
   * @param alreadyEncoded true to leave '%' as-is; false to convert it to '%25'.
   * @param strict true to encode '%' if it is not the prefix of a valid percent encoding.
   * @param plusIsSpace true to encode '+' as "%2B" if it is not already encoded.
   * @param unicodeAllowed true to leave non-ASCII codepoint unencoded.
   * @param charset which charset to use, null equals UTF-8.
   */
  static canonicalize(
    input: string,
    pos: number = 0,
    limit: number = input.length,
    encodeSet: string,
    alreadyEncoded: boolean = false,
    strict: boolean = false,
    plusIsSpace: boolean = false,
    unicodeAllowed: boolean = false,
    charset: string | null = null
  ): string {
    let codePoint: number;
    let i = pos;
    
    while (i < limit) {
      codePoint = input.charCodeAt(i);
      
      if (codePoint < 0x20 ||
          codePoint === 0x7f ||
          (codePoint >= 0x80 && !unicodeAllowed) ||
          encodeSet.indexOf(String.fromCharCode(codePoint)) !== -1 ||
          (codePoint === 0x25 && (!alreadyEncoded || (strict && !HttpUrl.isPercentEncoded(input, i, limit)))) ||
          (codePoint === 0x2B && plusIsSpace)) {
        // Slow path: the character at i requires encoding!
        let result = input.substring(pos, i);
        result += HttpUrl.writeCanonicalized(
          input,
          i,
          limit,
          encodeSet,
          alreadyEncoded,
          strict,
          plusIsSpace,
          unicodeAllowed,
          charset
        );
        return result;
      }
      i++;
    }
    
    // Fast path: no characters in [pos..limit) required encoding.
    return input.substring(pos, limit);
  }

  /**
   * Helper function to write canonicalized string (equivalent to Buffer.writeCanonicalized in Kotlin).
   */
  private static writeCanonicalized(
    input: string,
    pos: number,
    limit: number,
    encodeSet: string,
    alreadyEncoded: boolean,
    strict: boolean,
    plusIsSpace: boolean,
    unicodeAllowed: boolean,
    charset: string | null
  ): string {
    let result = '';
    const charsetName = charset || 'utf-8';
    let i = pos;
    
    while (i < limit) {
      const codePoint = input.charCodeAt(i);
      
      if (alreadyEncoded && (codePoint === 0x09 || codePoint === 0x0a || 
                             codePoint === 0x0c || codePoint === 0x0d)) {
        // Skip this character (tab, newline, form feed, carriage return)
        i++;
        continue;
      } else if (codePoint === 0x2B && plusIsSpace) {
        // Encode '+' as '%2B' since we permit ' ' to be encoded as either '+' or '%20'.
        result += alreadyEncoded ? '+' : '%2B';
        i++;
      } else if (codePoint < 0x20 ||
                 codePoint === 0x7f ||
                 (codePoint >= 0x80 && !unicodeAllowed) ||
                 encodeSet.indexOf(String.fromCharCode(codePoint)) !== -1 ||
                 (codePoint === 0x25 && (!alreadyEncoded || (strict && !HttpUrl.isPercentEncoded(input, i, limit))))) {
        // Percent encode this character
        const charToEncode = input.charAt(i);
        const encoder = new util.TextEncoder(charsetName);
        const bytes = encoder.encodeInto(charToEncode);
        
        for (let j = 0; j < bytes.length; j++) {
          const b = bytes[j] & 0xff;
          result += '%';
          result += HttpUrl.HEX_DIGITS[(b >> 4) & 0xf];
          result += HttpUrl.HEX_DIGITS[b & 0xf];
        }
        i++;
      } else {
        // This character doesn't need encoding. Just copy it over.
        result += String.fromCharCode(codePoint);
        i++;
      }
    }
    
    return result;
  }

  private static parseHexDigit(c: string): number {
    const code = c.charCodeAt(0);
    if (code >= 0x30 && code <= 0x39) return code - 0x30; // 0-9
    if (code >= 0x41 && code <= 0x46) return code - 0x41 + 10; // A-F
    if (code >= 0x61 && code <= 0x66) return code - 0x61 + 10; // a-f
    return -1;
  }

  private static isPercentEncoded(input: string, pos: number, limit: number): boolean {
    return pos + 2 < limit &&
           input.charAt(pos) === '%' &&
           HttpUrl.parseHexDigit(input.charAt(pos + 1)) !== -1 &&
           HttpUrl.parseHexDigit(input.charAt(pos + 2)) !== -1;
  }
}

/**
 * Builder for HttpUrl instances.
 */
export class HttpUrlBuilder {
  $scheme: string | null = null;
  $encodedUsername: string = '';
  $encodedPassword: string = '';
  $host: string | null = null;
  $port: number = -1;
  $encodedPathSegments: string[] = [''];
  $encodedQueryNamesAndValues: (string | null)[] | null = null;
  $encodedFragment: string | null = null;

  scheme(scheme: string): HttpUrlBuilder {
    if (scheme.toLowerCase() === 'http') {
      this.$scheme = 'http';
    } else if (scheme.toLowerCase() === 'https') {
      this.$scheme = 'https';
    } else {
      throw new IllegalArgumentException(`unexpected scheme: ${scheme}`);
    }
    return this;
  }

  username(username: string): HttpUrlBuilder {
    this.$encodedUsername = HttpUrl.canonicalize(username, 0, username.length, HttpUrl['USERNAME_ENCODE_SET']);
    return this;
  }

  encodedUsername(encodedUsername: string): HttpUrlBuilder {
    this.$encodedUsername = HttpUrl.canonicalize(encodedUsername, 0, encodedUsername.length, 
      HttpUrl['USERNAME_ENCODE_SET'], true);
    return this;
  }

  password(password: string): HttpUrlBuilder {
    this.$encodedPassword = HttpUrl.canonicalize(password, 0, password.length, HttpUrl['PASSWORD_ENCODE_SET']);
    return this;
  }

  encodedPassword(encodedPassword: string): HttpUrlBuilder {
    this.$encodedPassword = HttpUrl.canonicalize(encodedPassword, 0, encodedPassword.length,
      HttpUrl['PASSWORD_ENCODE_SET'], true);
    return this;
  }

  host(host: string): HttpUrlBuilder {
    const decoded = HttpUrl.percentDecode(host);
    const canonical = toCanonicalHost(decoded);
    if (canonical === null) {
      throw new IllegalArgumentException(`unexpected host: ${host}`);
    }
    this.$host = canonical;
    return this;
  }

  port(port: number): HttpUrlBuilder {
    kotlinRequire(port >= 1 && port <= 65535, `unexpected port: ${port}`)
    this.$port = port;
    return this;
  }

  addPathSegment(pathSegment: string): HttpUrlBuilder {
    this.push(pathSegment, 0, pathSegment.length, false, false);
    return this;
  }

  addPathSegments(pathSegments: string): HttpUrlBuilder {
    return this.addPathSegmentsInternal(pathSegments, false);
  }

  addEncodedPathSegment(encodedPathSegment: string): HttpUrlBuilder {
    this.push(encodedPathSegment, 0, encodedPathSegment.length, false, true);
    return this;
  }

  addEncodedPathSegments(encodedPathSegments: string): HttpUrlBuilder {
    return this.addPathSegmentsInternal(encodedPathSegments, true);
  }

  /**
   * Adds multiple path segments from an array.
   */
  addPathSegmentsArray(pathSegments: string[]): HttpUrlBuilder {
    for (const segment of pathSegments) {
      this.addPathSegment(segment);
    }
    return this;
  }

  /**
   * Sets all path segments, replacing existing ones.
   */
  setPathSegments(pathSegments: string[]): HttpUrlBuilder {
    this.$encodedPathSegments = [''];
    return this.addPathSegmentsArray(pathSegments);
  }

  /**
   * Sets all encoded path segments, replacing existing ones.
   */
  setEncodedPathSegments(encodedPathSegments: string[]): HttpUrlBuilder {
    this.$encodedPathSegments = [''];
    for (const segment of encodedPathSegments) {
      this.addEncodedPathSegment(segment);
    }
    return this;
  }

  private addPathSegmentsInternal(pathSegments: string, alreadyEncoded: boolean): HttpUrlBuilder {
    let offset = 0;
    do {
      const segmentEnd = this.delimiterOffsetPath(pathSegments, '/\\', offset, pathSegments.length);
      const addTrailingSlash = segmentEnd < pathSegments.length;
      this.push(pathSegments, offset, segmentEnd, addTrailingSlash, alreadyEncoded);
      offset = segmentEnd + 1;
    } while (offset <= pathSegments.length);
    return this;
  }

  setPathSegment(index: number, pathSegment: string): HttpUrlBuilder {
    const canonicalPathSegment = HttpUrl.canonicalize(pathSegment, 0, pathSegment.length, HttpUrl['PATH_SEGMENT_ENCODE_SET']);
    kotlinRequire(!this.isDot(canonicalPathSegment) && !this.isDotDot(canonicalPathSegment),
      `unexpected path segment: ${pathSegment}`)
    this.$encodedPathSegments[index] = canonicalPathSegment;
    return this;
  }

  setEncodedPathSegment(index: number, encodedPathSegment: string): HttpUrlBuilder {
    const canonicalPathSegment = HttpUrl.canonicalize(encodedPathSegment, 0, encodedPathSegment.length,
      HttpUrl['PATH_SEGMENT_ENCODE_SET'], true);
    kotlinRequire(!this.isDot(canonicalPathSegment) && !this.isDotDot(canonicalPathSegment),
      "unexpected path segment: $encodedPathSegment")
    this.$encodedPathSegments[index] = canonicalPathSegment;
    return this;
  }

  removePathSegment(index: number): HttpUrlBuilder {
    this.$encodedPathSegments.splice(index, 1);
    if (this.$encodedPathSegments.length === 0) {
      this.$encodedPathSegments.push('');
    }
    return this;
  }

  encodedPath(encodedPath: string): HttpUrlBuilder {
    kotlinRequire(encodedPath.startsWith('/'), `unexpected encodedPath: ${encodedPath}`)
    this.resolvePath(encodedPath, 0, encodedPath.length);
    return this;
  }
f
  query(query: string | null): HttpUrlBuilder {
    this.$encodedQueryNamesAndValues = query !== null ?
      HttpUrl.toQueryNamesAndValues(HttpUrl.canonicalize(query, 0, query.length, HttpUrl['QUERY_ENCODE_SET'], false, false, true)) :
      null;
    return this;
  }

  encodedQuery(encodedQuery: string | null): HttpUrlBuilder {
    this.$encodedQueryNamesAndValues = encodedQuery !== null ?
      HttpUrl.toQueryNamesAndValues(HttpUrl.canonicalize(encodedQuery, 0, encodedQuery.length,
        HttpUrl['QUERY_ENCODE_SET'], true, false, true)) :
      null;
    return this;
  }

  addQueryParameter(name: string, value: string | null): HttpUrlBuilder {
    if (this.$encodedQueryNamesAndValues === null) {
      this.$encodedQueryNamesAndValues = [];
    }
    this.$encodedQueryNamesAndValues.push(
      HttpUrl.canonicalize(name, 0, name.length, HttpUrl['QUERY_COMPONENT_ENCODE_SET'], false, false, true)
    );
    this.$encodedQueryNamesAndValues.push(
      value !== null ? HttpUrl.canonicalize(value, 0, value.length, HttpUrl['QUERY_COMPONENT_ENCODE_SET'], false, false, true) : null
    );
    return this;
  }

  addEncodedQueryParameter(encodedName: string, encodedValue: string | null): HttpUrlBuilder {
    if (this.$encodedQueryNamesAndValues === null) {
      this.$encodedQueryNamesAndValues = [];
    }
    this.$encodedQueryNamesAndValues.push(
      HttpUrl.canonicalize(encodedName, 0, encodedName.length, HttpUrl['QUERY_COMPONENT_REENCODE_SET'], true, false, true)
    );
    this.$encodedQueryNamesAndValues.push(
      encodedValue !== null ? HttpUrl.canonicalize(encodedValue, 0, encodedValue.length,
        HttpUrl['QUERY_COMPONENT_REENCODE_SET'], true, false, true) : null
    );
    return this;
  }

  setQueryParameter(name: string, value: string | null): HttpUrlBuilder {
    this.removeAllQueryParameters(name);
    this.addQueryParameter(name, value);
    return this;
  }

  setEncodedQueryParameter(encodedName: string, encodedValue: string | null): HttpUrlBuilder {
    this.removeAllEncodedQueryParameters(encodedName);
    this.addEncodedQueryParameter(encodedName, encodedValue);
    return this;
  }

  removeAllQueryParameters(name: string): HttpUrlBuilder {
    if (this.$encodedQueryNamesAndValues === null) return this;
    const nameToRemove = HttpUrl.canonicalize(name, 0, name.length, HttpUrl['QUERY_COMPONENT_ENCODE_SET'], false, false, true);
    this.removeAllCanonicalQueryParameters(nameToRemove);
    return this;
  }

  removeAllEncodedQueryParameters(encodedName: string): HttpUrlBuilder {
    if (this.$encodedQueryNamesAndValues === null) return this;
    const nameToRemove = HttpUrl.canonicalize(encodedName, 0, encodedName.length,
      HttpUrl['QUERY_COMPONENT_REENCODE_SET'], true, false, true);
    this.removeAllCanonicalQueryParameters(nameToRemove);
    return this;
  }

  private removeAllCanonicalQueryParameters(canonicalName: string): void {
    for (let i = this.$encodedQueryNamesAndValues!.length - 2; i >= 0; i -= 2) {
      if (canonicalName === this.$encodedQueryNamesAndValues![i]) {
        this.$encodedQueryNamesAndValues!.splice(i, 2);
        if (this.$encodedQueryNamesAndValues!.length === 0) {
          this.$encodedQueryNamesAndValues = null;
          return;
        }
      }
    }
  }

  fragment(fragment: string | null): HttpUrlBuilder {
    this.$encodedFragment = fragment !== null ?
      HttpUrl.canonicalize(fragment, 0, fragment.length, HttpUrl['FRAGMENT_ENCODE_SET'], false, false, false, true) :
      null;
    return this;
  }

  encodedFragment(encodedFragment: string | null): HttpUrlBuilder {
    this.$encodedFragment = encodedFragment !== null ?
      HttpUrl.canonicalize(encodedFragment, 0, encodedFragment.length, HttpUrl['FRAGMENT_ENCODE_SET'], true, false, true) :
      null;
    return this;
  }

  /**
   * Re-encodes the components of this URL so that it satisfies (obsolete) RFC 2396, which is
   * particularly strict for certain components. Used for URI conversion.
   */
  reencodeForUri(): HttpUrlBuilder {
    // Re-encode host to remove forbidden characters
    if (this.$host !== null) {
      this.$host = this.$host.replace(/["<>^`{|}]/g, '');
    }

    // Re-encode path segments with stricter encoding
    for (let i = 0; i < this.$encodedPathSegments.length; i++) {
      this.$encodedPathSegments[i] = HttpUrl.canonicalize(
        this.$encodedPathSegments[i],
        0,
        this.$encodedPathSegments[i].length,
        HttpUrl['PATH_SEGMENT_ENCODE_SET_URI'],
        true,
        true
      );
    }

    // Re-encode query parameters with stricter encoding
    if (this.$encodedQueryNamesAndValues !== null) {
      for (let i = 0; i < this.$encodedQueryNamesAndValues.length; i++) {
        const item = this.$encodedQueryNamesAndValues[i];
        if (item !== null) {
          this.$encodedQueryNamesAndValues[i] = HttpUrl.canonicalize(
            item,
            0,
            item.length,
            HttpUrl['QUERY_COMPONENT_ENCODE_SET_URI'],
            true,
            true,
            true
          );
        }
      }
    }

    // Re-encode fragment with stricter encoding
    if (this.$encodedFragment !== null) {
      this.$encodedFragment = HttpUrl.canonicalize(
        this.$encodedFragment,
        0,
        this.$encodedFragment.length,
        HttpUrl['FRAGMENT_ENCODE_SET_URI'],
        true,
        true,
        true
      );
    }

    return this;
  }

  build(): HttpUrl {
    if (this.$scheme === null) throw new IllegalStateException('scheme == null');
    if (this.$host === null) throw new IllegalStateException('host == null');

    const effectivePort = this.$port !== -1 ? this.$port : HttpUrl.defaultPort(this.$scheme);
    const pathSegments = this.$encodedPathSegments.map(seg => HttpUrl.percentDecode(seg));
    const queryNamesAndValues = this.$encodedQueryNamesAndValues !== null ?
      this.$encodedQueryNamesAndValues.map(item => item !== null ? HttpUrl.percentDecode(item, 0, item.length, true) : null) :
      null;
    const fragment = this.$encodedFragment !== null ? HttpUrl.percentDecode(this.$encodedFragment) : null;

    return new HttpUrl(
      this.$scheme,
      HttpUrl.percentDecode(this.$encodedUsername),
      HttpUrl.percentDecode(this.$encodedPassword),
      this.$host,
      effectivePort,
      pathSegments,
      queryNamesAndValues,
      fragment,
      this.toString()
    );
  }

  toString(): string {
    let result = '';
    
    if (this.$scheme !== null) {
      result += this.$scheme;
      result += '://';
    } else {
      result += '//';
    }

    if (this.$encodedUsername.length > 0 || this.$encodedPassword.length > 0) {
      result += this.$encodedUsername;
      if (this.$encodedPassword.length > 0) {
        result += ':';
        result += this.$encodedPassword;
      }
      result += '@';
    }

    if (this.$host !== null) {
      if (this.$host.indexOf(':') !== -1) {
        result += '[';
        result += this.$host;
        result += ']';
      } else {
        result += this.$host;
      }
    }

    if (this.$port !== -1 || this.$scheme !== null) {
      const effectivePort = this.effectivePort();
      if (this.$scheme === null || effectivePort !== HttpUrl.defaultPort(this.$scheme)) {
        result += ':';
        result += effectivePort;
      }
    }

    for (let i = 0; i < this.$encodedPathSegments.length; i++) {
      result += '/';
      result += this.$encodedPathSegments[i];
    }

    if (this.$encodedQueryNamesAndValues !== null) {
      result += '?';
      result += HttpUrl.toQueryString(this.$encodedQueryNamesAndValues);
    }

    if (this.$encodedFragment !== null) {
      result += '#';
      result += this.$encodedFragment;
    }

    return result;
  }

  parse(base: HttpUrl | null, input: string): HttpUrlBuilder {
    let pos = indexOfFirstNonAsciiWhitespace(input, 0, input.length);
    const limit = indexOfLastNonAsciiWhitespace(input, pos, input.length);

    // Scheme
    const schemeDelimiterOffset = this.schemeDelimiterOffset(input, pos, limit);
    if (schemeDelimiterOffset !== -1) {
      if (input.substring(pos, pos + 6).toLowerCase() === 'https:') {
        this.$scheme = 'https';
        pos += 6;
      } else if (input.substring(pos, pos + 5).toLowerCase() === 'http:') {
        this.$scheme = 'http';
        pos += 5;
      } else {
        throw new IllegalArgumentException(`Expected URL scheme 'http' or 'https' but was '${input.substring(0, schemeDelimiterOffset)}'`);
      }
    } else if (base !== null) {
      this.$scheme = base.scheme;
    } else {
      throw new IllegalArgumentException('Expected URL scheme \'http\' or \'https\' but no colon was found');
    }

    // Authority
    let hasUsername = false;
    let hasPassword = false;
    const slashCount = this.slashCount(input, pos, limit);
    
    if (slashCount >= 2 || base === null || base.scheme !== this.$scheme) {
      pos += slashCount;
      
      authority: while (true) {
        const componentDelimiterOffset = this.delimiterOffsetPath(input, '@/\\?#', pos, limit);
        const c = componentDelimiterOffset !== limit ? input.charCodeAt(componentDelimiterOffset) : -1;
        
        if (c === 0x40) { // '@'
          if (!hasPassword) {
            const passwordColonOffset = this.delimiterOffsetChar(input, ':', pos, componentDelimiterOffset);
            const canonicalUsername = HttpUrl.canonicalize(input, pos, passwordColonOffset, HttpUrl['USERNAME_ENCODE_SET'], true);
            this.$encodedUsername = hasUsername ? this.$encodedUsername + '%40' + canonicalUsername : canonicalUsername;
            
            if (passwordColonOffset !== componentDelimiterOffset) {
              hasPassword = true;
              this.$encodedPassword = HttpUrl.canonicalize(input, passwordColonOffset + 1, componentDelimiterOffset,
                HttpUrl['PASSWORD_ENCODE_SET'], true);
            }
            hasUsername = true;
          } else {
            this.$encodedPassword = this.$encodedPassword + '%40' +
              HttpUrl.canonicalize(input, pos, componentDelimiterOffset, HttpUrl['PASSWORD_ENCODE_SET'], true);
          }
          pos = componentDelimiterOffset + 1;
        } else if (c === -1 || c === 0x2F || c === 0x5C || c === 0x3F || c === 0x23) {
          // Host info
          const portColonOffset = this.portColonOffset(input, pos, componentDelimiterOffset);
          if (portColonOffset + 1 < componentDelimiterOffset) {
            const decodedHost = HttpUrl.percentDecode(input, pos, portColonOffset);
            const canonicalHost = toCanonicalHost(decodedHost);
            kotlinRequire(canonicalHost !== null, `Invalid URL host: "${decodedHost}"`)
            this.$host = canonicalHost;
            this.$port = this.parsePort(input, portColonOffset + 1, componentDelimiterOffset);
            kotlinRequire(this.$port !== -1, `Invalid URL port: "${input.substring(portColonOffset + 1, componentDelimiterOffset)}"`)
          } else {
            const decodedHost = HttpUrl.percentDecode(input, pos, portColonOffset);
            const canonicalHost = toCanonicalHost(decodedHost);
            kotlinRequire(canonicalHost !== null, `Invalid URL host: "${decodedHost}"`)
            this.$host = canonicalHost;
            this.$port = HttpUrl.defaultPort(this.$scheme!);
          }
          kotlinRequire(this.$host !== null, `Invalid URL host: "${input.substring(pos, portColonOffset)}"`)
          pos = componentDelimiterOffset;
          break authority;
        }
      }
    } else {
      // Relative link
      this.$encodedUsername = base.encodedUsername;
      this.$encodedPassword = base.encodedPassword;
      this.$host = base.host;
      this.$port = base.port;
      this.$encodedPathSegments = [...base.encodedPathSegments];
      if (pos === limit || input.charAt(pos) === '#') {
        this.encodedQuery(base.encodedQuery);
      }
    }

    // Path
    const pathDelimiterOffset = this.delimiterOffsetPath(input, '?#', pos, limit);
    this.resolvePath(input, pos, pathDelimiterOffset);
    pos = pathDelimiterOffset;

    // Query
    if (pos < limit && input.charAt(pos) === '?') {
      const queryDelimiterOffset = this.delimiterOffsetChar(input, '#', pos, limit);
      this.$encodedQueryNamesAndValues = HttpUrl.toQueryNamesAndValues(
        HttpUrl.canonicalize(input, pos + 1, queryDelimiterOffset, HttpUrl['QUERY_ENCODE_SET'], true, false, true)
      );
      pos = queryDelimiterOffset;
    }

    // Fragment
    if (pos < limit && input.charAt(pos) === '#') {
      this.$encodedFragment = HttpUrl.canonicalize(input, pos + 1, limit, HttpUrl['FRAGMENT_ENCODE_SET'], true, false, true);
    }

    return this;
  }

  private effectivePort(): number {
    return this.$port !== -1 ? this.$port : HttpUrl.defaultPort(this.$scheme!);
  }

  private resolvePath(input: string, startPos: number, limit: number): void {
    let pos = startPos;
    if (pos === limit) {
      return;
    }
    
    const c = input.charAt(pos);
    if (c === '/' || c === '\\') {
      // Absolute path: reset to the default "/".
      this.$encodedPathSegments = [''];
      pos++;
    } else {
      // Relative path: clear everything after the last '/'.
      // Per RFC 3986, the last segment is considered a file, not a directory.
      this.$encodedPathSegments[this.$encodedPathSegments.length - 1] = '';
    }

    let i = pos;
    while (i < limit) {
      const pathSegmentDelimiterOffset = this.delimiterOffsetPath(input, '/\\', i, limit);
      const segmentHasTrailingSlash = pathSegmentDelimiterOffset < limit;
      this.push(input, i, pathSegmentDelimiterOffset, segmentHasTrailingSlash, true);
      i = pathSegmentDelimiterOffset;
      if (segmentHasTrailingSlash) i++;
    }
  }

  private push(input: string, pos: number, limit: number, addTrailingSlash: boolean, alreadyEncoded: boolean): void {
    const segment = HttpUrl.canonicalize(input, pos, limit, HttpUrl['PATH_SEGMENT_ENCODE_SET'], alreadyEncoded);
    
    if (this.isDot(segment)) {
      return;
    }
    if (this.isDotDot(segment)) {
      this.pop();
      return;
    }
    
    if (this.$encodedPathSegments[this.$encodedPathSegments.length - 1].length === 0) {
      this.$encodedPathSegments[this.$encodedPathSegments.length - 1] = segment;
    } else {
      this.$encodedPathSegments.push(segment);
    }
    
    if (addTrailingSlash) {
      this.$encodedPathSegments.push('');
    }
  }

  private isDot(input: string): boolean {
    return input === '.' || input.toLowerCase() === '%2e';
  }

  private isDotDot(input: string): boolean {
    return input === '..' ||
           input.toLowerCase() === '%2e.' ||
           input.toLowerCase() === '.%2e' ||
           input.toLowerCase() === '%2e%2e';
  }

  /**
   * Removes a path segment. When this method returns the last segment is always "", which means
   * the encoded path will have a trailing '/'.
   *
   * Popping "/a/b/c/" yields "/a/b/". In this case the list of path segments goes from ["a",
   * "b", "c", ""] to ["a", "b", ""].
   *
   * Popping "/a/b/c" also yields "/a/b/". The list of path segments goes from ["a", "b", "c"]
   * to ["a", "b", ""].
   */
  private pop(): void {
    const removed = this.$encodedPathSegments.pop()!;
    
    // Make sure the path ends with a '/' by either adding an empty string or clearing a segment.
    if (removed.length === 0 && this.$encodedPathSegments.length > 0) {
      this.$encodedPathSegments[this.$encodedPathSegments.length - 1] = '';
    } else {
      this.$encodedPathSegments.push('');
    }
  }

  private schemeDelimiterOffset(input: string, pos: number, limit: number): number {
    if (limit - pos < 2) return -1;
    
    const c0 = input.charAt(pos);
    if ((c0 < 'a' || c0 > 'z') && (c0 < 'A' || c0 > 'Z')) return -1;
    
    for (let i = pos + 1; i < limit; i++) {
      const c = input.charAt(i);
      if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '+' || c === '-' || c === '.') {
        continue;
      } else if (c === ':') {
        return i;
      } else {
        return -1;
      }
    }
    return -1;
  }

  private slashCount(input: string, pos: number, limit: number): number {
    let slashCount = 0;
    for (let i = pos; i < limit; i++) {
      const c = input.charAt(i);
      if (c === '\\' || c === '/') {
        slashCount++;
      } else {
        break;
      }
    }
    return slashCount;
  }

  private portColonOffset(input: string, pos: number, limit: number): number {
    let i = pos;
    while (i < limit) {
      const c = input.charAt(i);
      if (c === '[') {
        while (++i < limit) {
          if (input.charAt(i) === ']') break;
        }
      } else if (c === ':') {
        return i;
      }
      i++;
    }
    return limit;
  }

  private parsePort(input: string, pos: number, limit: number): number {
    try {
      const portString = HttpUrl.canonicalize(input, pos, limit, '');
      const port = parseInt(portString, 10);
      if (port >= 1 && port <= 65535) return port;
      return -1;
    } catch (e) {
      return -1;
    }
  }

  private delimiterOffsetPath(input: string, delimiters: string, pos: number, limit: number): number {
    for (let i = pos; i < limit; i++) {
      if (delimiters.indexOf(input.charAt(i)) !== -1) {
        return i;
      }
    }
    return limit;
  }

  private delimiterOffsetChar(input: string, delimiter: string, pos: number, limit: number): number {
    for (let i = pos; i < limit; i++) {
      if (input.charAt(i) === delimiter) {
        return i;
      }
    }
    return limit;
  }
}

// Add Builder alias to HttpUrl for compatibility
// This allows usage like: HttpUrl.Builder.defaultPort()
HttpUrl.Builder = HttpUrlBuilder;


